Capítulo 23. A Transformada Watershed
Observe a imagem da Figura 57, “Grãos conectados por pequenas pontes de pixels.”. Trata-se de uma imagem de um conjunto de grãos de feijão colocados em um anteparo branco (esquerda) e sua correspondente limiarização pelo método de Otsu (direita). Perceba que, na limiarização, muitos feijões aparecem conectados por pequenas pontes de pixels. Isso ocorre porque o processo de limiarização desconsidera aspectos de conectividade, levando em conta apenas a intensidade dos pixels.
Entretanto, se o objetivo da limiarização é uma posterior contagem de grãos pela extração de componentes conectadas, o processo de limiarização sozinho não é suficiente. Neste exemplo, a limiarização é capaz separar os grãos do fundo, mas não permite que os grãos conectados sejam também separados. Para resolver este problema, uma etapa posterior de segmentação é necessária para determinar as fronteiras entre os grãos. Uma técnica comumente utilizada para este fim é a transformada watershed.
A transformada watershed é uma técnica de segmentação que se baseia na ideia de que a imagem é vista como um relevo topográfico, onde os pixels de intensidade mais baixa correspondem a vales e os pixels de intensidade mais alta correspondem a picos.
Considere a imagem da Figura 58, “Perfil de intensidade de uma imagem com regiões de vales e picos.”, que formada por uma região de tonalidade clara permeada por algumas regiões escuras no seu interior.
Quando interpretada como um relevo topográfico, a imagem fica conforme apresentado no gráfico a seguir.
A ideia da transformada watershed é que, se os vales forem inundados com água, esta subirá até encontrar os picos. Os corpos d’água, na medida em que a inundação vai aumentando acabam se encontrando nos pontos de sela que, por analogia, são justamente aqueles que fazem de separação entre os vales na imagem. Neste contexto, é papel da transformada identificar os pontos onde os corpos d’água se encontram, marcando-os, e com isso determinar as fronteiras (diques) entre as regiões.
Por exemplo, para o relevo da Figura 58, “Perfil de intensidade de uma imagem com regiões de vales e picos.”, a transformada watershed é capaz de identificar as fronteiras entre as regiões de vales e picos conforme apresentado no gráfico a seguir, onde a superfície azul com cristas amarelas representa os diques determinados pela transformada watershed.
Para que a transformada watershed produza um bom resultado, é necessário que os vales e picos sejam bem definidos, caso contrário pequenas variações de intensidade podem levar a segmentações indesejadas, sendo criados diques onde não deveriam existir. Algumas estratégias podem ser adotadas para esse fim. Por exemplo, suavizar a imagem previamente e depois aplicar um filtro do gradiente para evidenciar os vales e picos.
Entretanto, uma das formas mais bem sucedidas de evitar a supersegmentação é a utilização de marcadores para guiar a transformada watershed. Os marcadores são regiões conhecidas que determinam a posição dos vales e picos e uma maneira de obter tais marcadores é utilizar a transformada distância, uma vez que ela é capaz de fornecer uma boa aproximação para os marcadores dos centros dos objetos que se tocam.
Nesta lição, será apresentado um exemplo de aplicação da transformada watershed para segmentar pixels com grãos de feijão presentes em uma imagem. No exemplo mostrada na Figura 59, “Imagem com feijões e o resultado da limiarização pelo método de Otsu.”, após a limiarização automática pelo método de Otsu, duas componentes conectadas foram produzidas como resultado na saída ao invés de 10 componentes, uma para cada feijão, como seria esperado.
Podem ser encontradas diversas implementações na literatura para a transformada watershed. Entretanto, a implementação do OpenCV utiliza uma transformação baseada em marcadores. A função watershed() recebe como entrada uma imagem e um conjunto de marcadores - internos e externos - indicando onde certamente há alguma componente conectada e onde certamente não há uma componente conectada (no caso, o fundo da imagem). Cada marcador é um número inteiro que identifica a região à qual devem pertencer os pixels de uma dada conectada. A função então realiza a segmentação da imagem baseada nos marcadores fornecidos.
A filtragem de forma pode ser utilizada para corrigir esse problema usando o programa watershed.cpp, que é mostrado na Listagem 73, “watershed.cpp”.
#include <chrono>
#include <iostream>
#include <opencv2/opencv.hpp>
int main(int argc, char* argv[]) {
cv::Mat dist;
cv::Mat bw;
cv::Mat src;
cv::Mat dist_8u;
cv::Mat background;
double min, max;
int numComponents;
src = cv::imread(argv[1], cv::IMREAD_COLOR);
if (!src.data) {
return -1;
}
cv::namedWindow("Segmentacao de Otsu", cv::WINDOW_NORMAL);
cv::namedWindow("Marcadores Interiores", cv::WINDOW_NORMAL);
cv::namedWindow("Marcadores de Fundo", cv::WINDOW_NORMAL);
cv::namedWindow("Watershed", cv::WINDOW_NORMAL);
cv::namedWindow("Watershed Lines", cv::WINDOW_NORMAL);
cv::cvtColor(src, bw, cv::COLOR_BGR2GRAY);
cv::threshold(bw, bw, 0, 255,
cv::THRESH_BINARY + cv::THRESH_OTSU + cv::THRESH_BINARY_INV);
cv::distanceTransform(bw, dist, cv::DIST_L2, 3);
cv::minMaxLoc(dist, &min, &max);
cv::threshold(dist, dist, max * 0.65, 255, cv::THRESH_BINARY);
dist.convertTo(dist_8u, CV_8U);
cv::Mat markers = cv::Mat::zeros(dist.size(), CV_32SC1);
numComponents = cv::connectedComponents(dist_8u, markers, 8);
cv::Mat interior_markers = src.clone();
cv::Mat bg_markers = src.clone();
cv::bitwise_not(bw, bw);
cv::erode(bw, background, cv::Mat(), cv::Point(-1, -1), 2);
for (int i = 0; i < background.rows; i++) {
for (int j = 0; j < background.cols; j++) {
if (background.at<uchar>(i, j) > 0) {
bg_markers.at<cv::Vec3b>(i, j) = cv::Vec3b(255, 0, 0);
markers.at<int>(i, j) = INT32_MAX;
}
if (dist_8u.at<uchar>(i, j) == 255) {
interior_markers.at<cv::Vec3b>(i, j) = cv::Vec3b(0, 255, 0);
}
}
}
cv::watershed(src, markers);
std::vector<cv::Vec3b> colors;
std::srand(time(0));
for (int i = 0; i < numComponents; i++) {
uchar b = std::rand() % 256;
uchar g = std::rand() % 256;
uchar r = std::rand() % 256;
colors.push_back(cv::Vec3b((uchar)b, (uchar)g, (uchar)r));
}
cv::Mat watershed_result = cv::Mat::zeros(markers.size(), CV_8UC3);
cv::Mat watershed_markers = cv::Mat::zeros(src.size(), CV_8UC3);
for (int i = 0; i < markers.rows; i++) {
for (int j = 0; j < markers.cols; j++) {
int index = markers.at<int>(i, j);
if (index > 0 && index <= numComponents)
watershed_result.at<cv::Vec3b>(i, j) = colors[index - 1];
else {
watershed_result.at<cv::Vec3b>(i, j) = cv::Vec3b(0, 0, 0);
watershed_markers.at<cv::Vec3b>(i, j) = cv::Vec3b(255, 255, 255);
}
}
}
cv::imshow("Segmentacao de Otsu", bw);
cv::imshow("Marcadores Interiores", interior_markers);
cv::imshow("Marcadores de Fundo", bg_markers);
cv::imshow("Watershed", watershed_result);
cv::imshow("Watershed Lines", watershed_markers);
cv::imwrite("watershed-background-markers.png", bg_markers);
cv::imwrite("watershed-interior-markers.png", interior_markers);
cv::imwrite("watershed-lines.png", watershed_markers);
cv::imwrite("watershed-output.png", watershed_result);
cv::waitKey(0);
return 0;
}
Para compilar e executar o programa-exemplo watershed.cpp, salve-o em um diretório juntamente com o arquivo CMakeLists.txt adaptado para o exemplo (Listagem 74, “CMakeLists.txt”), a imagem watershed-feijoes.png e execute a seqüência de comandos mostrada na Listagem 75, “Compilação do programa watershed”.
cmake_minimum_required(VERSION 3.0.0)
project(watershed VERSION 0.1.0 LANGUAGES C CXX)
find_package(OpenCV REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS})
add_executable(watershed watershed.cpp)
target_link_libraries(watershed ${OpenCV_LIBS})
$ mkdir build
$ cd build
$ cmake ..
$ make
$ ./watershed ../watershed-feijoes.png
A saída do programa watershed é mostrado na Figura 60, “Saída do programa watershed”, com a imagem original à esquerda e a imagem segmentada à direita. Perceba que agora os grãos de feijão estão separados, cada um em uma componente conectada distinta.
23.1. Descrição do programa watershed.cpp
Vencidas as etapas iniciais de carga e tratamento da imagem, o programa watershed.cpp transforma a imagem em escala de cinza e aplica a limiarização de Otsu.
cv::cvtColor(src, bw, cv::COLOR_BGR2GRAY);
cv::threshold(bw, bw, 0, 255,
cv::THRESH_BINARY + cv::THRESH_OTSU + cv::THRESH_BINARY_INV);
O resultado da limiarização é mostrado na Figura 61, “Imagem limiarizada pelo método de Otsu.”, onde os grãos de feijão estão separados do fundo da imagem.
A limiarização irá separar possíveis regiões de grãos do fundo da imagem. Perceba que, para o exemplo em questão, a limiarização automática não escolheu um valor de threshold capaz de separar os objetos do fundo e também não é suficiente para separar os grãos conectados entre si.
cv::distanceTransform(bw, dist, cv::DIST_L2, 3);
cv::minMaxLoc(dist, &min, &max);
cv::threshold(dist, dist, max * 0.65, 255, cv::THRESH_BINARY);
A etapa seguinte consiste em determinar marcadores para regiões que certamente contém os grãos individuais e certamente não contém os grãos. Algo que se sabe dos pontos interiores à região é que certamente ficam próximos aos centros dos feijões. A transformada distância é capaz de oferecer uma aproximação razoável para esses marcadores, uma vez que mapeia a distância da borda de um objeto até um ponto interno. Os pontos mais internos estão mais distantes da borda, de sorte que uma limiarização da transformada distância pode ser utilizada para obter os marcadores internos.
O valor do limiar é determinado a partir do valor máximo da transformada distância. Neste exemplo, o valor do limiar no exemplo foi de 65% do valor máximo, mas deve ser ajustado conforme a necessidade.
O resultado da transformada distância limiarizada é mostrado na Figura 62, “Marcadores internos obtidos pela transformada distância.”, destacando os marcadores internos selecionados nessa etapa. Observe os marcadores destacados sobre a imagem original, indicando os locais aproximados dos centros dos grãos de feijão.
cv::Mat markers = cv::Mat::zeros(dist.size(), CV_32SC1);
numComponents = cv::connectedComponents(dist_8u, markers, 8);
Obtidos os marcadores internos, é necessário que cada um deles seja rotulado com um número inteiro distinto. É essa forma que a transformada watershed identifica cada região de interesse de forma separada. Para isso, a função connectedComponents() é utilizada para rotular as componentes conectadas dos marcadores internos usando inteiros distintos.
cv::bitwise_not(bw, bw);
cv::erode(bw, background, cv::Mat(), cv::Point(-1, -1), 2);
markers.at<int>(i, j) = INT32_MAX;
Os marcadores externos, que delimitam a região fora dos grãos são obtidos invertendo a imagem limiarizada e realizando duas erosões morfológicas (último parâmetro da função erode()) no resultado. A erosão remove os pontos que tocam a borda externa dos grãos, garantindo que os pixels restantes sejam referentes ao fundo da imagem. Na imagem de marcadores, os pixels de fundo são rotulados com o limite superior INT32_MAX, para garantir que não sejam confundidos com os marcadores internos.
A imagem dos marcadores externos é mostrada na Figura 63, “Marcadores externos obtidos pela erosão do inverso da imagem limiarizada.”, onde os marcadores externos estão destacados em azul. Perceba nesta imagem que a iluminação da cena não contribuiu para uma segmentação perfeita dos grãos.
cv::watershed(src, markers);
O cálculo da transformada watershed é realizado. Como argumentos, a função watershed() recebe a imagem original e uma imagem contendo os marcadores internos e externos. Essa imagem de marcadores é formada por números inteiros, cada um representando uma região de interesse.
O restante do código se encarrega da exibição dos resultados e gravação das imagens para estudo. A imagem segmentada é mostrada na Figura 60, “Saída do programa watershed”, onde os grãos de feijão estão separados, cada um em uma componente conectada distinta.
23.2. Exercícios
-
Modifique o programa
watershed.cpppara que a transformada watershed seja aplicada em uma imagem watershed-cafe.png mostrada na Figura 64, “Imagem com grãos de café para aplicação da transformada watershed.”. Realize as adaptações que julgar necessárias, incluindo o uso de filtros adicionais e verifique a robustez da técnica, mostrando os resultados obtidos.